package com.songyang.yougnsql.core.data.pageindex;

import com.songyang.yougnsql.common.ErrorMsg;
import com.songyang.yougnsql.common.ExceptionHandler;
import com.songyang.yougnsql.common.YoungSQLException;
import com.songyang.yougnsql.core.data.page.Page;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName pageIndex
 * @Description 页面索引
 * 缓存了每一页的空闲位置
 * 用于在上层模块进入插入时快速找到一个合适的页面
 * 而无需从磁盘或缓存中检查每一个页面的信息
 * @date 2022/12/4 3:03 PM
 * @Author yanceysong
 * @Version 1.0
 */
public class PageIndex {

    /**
     * 将一页划分为40个空间
     * 比如8k划分为8份，他的区间的下限值为1k
     * 那么划分的区间为[0k-1k],[1k-2k],[2k-3k]...
     */
    private static final int INTERVALS = 40;
    /**
     * 划分空间的下限值
     */
    private static final int THRESHOLD = Page.PAGE_SIZE / INTERVALS;
    private final Lock lock;
    private final List<PageInfo>[] pageInfoList;

    public PageIndex() {
        lock = new ReentrantLock();
        //这里多分配一个区间，是为了防止select的时候报空指针
        //具体内容查看select方法的注释
        pageInfoList = new List[INTERVALS + 1];
        for (int i = 0; i < INTERVALS + 1; i++) {
            pageInfoList[i] = (new ArrayList<>());
        }
    }

    /**
     * 向pageIndex中添加页面信息
     *
     * @param pageNumber 页面号
     * @param freeSpace  页面空闲的大小
     */
    public void add(int pageNumber, int freeSpace) {
        lock.lock();
        try {
            //根据空闲的大小计算索引的坐标，找到被分配的区间的位置
            int index = freeSpace / THRESHOLD;
            if (index > INTERVALS) {
                throw new YoungSQLException(ErrorMsg.DATA_TOO_LARGE_EXCEPTION);
            }
            //记录下这个区间的位置信息
            pageInfoList[index].add(new PageInfo(pageNumber, freeSpace));
        } catch (YoungSQLException e) {
            ExceptionHandler.handler(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 在索引当中根据需要的空间大小，找到合适的页面
     * 算出区间号然后判断该区间是否存在空闲页面，如果存在则直接返回，不存在就去更大的区间找
     * 可以看到被选取的页直接从PageIndex中移除，这意味着同一个页面不允许并发写
     *
     * @param spaceSize 需要的空间大小
     * @return 合适的页面, 为null则没有找到
     */
    public PageInfo select(int spaceSize) {
        lock.lock();
        try {
            //算出区间号
            int index = spaceSize / THRESHOLD;
            /*
             *这里着重介绍一下为什么要自加
             *比如8k划分为8分，他的区间的下限值为1k
             * 那么划分的区间为[0k-1k],[1k-2k],[2k-3k]...
             * 如果我需要1.5k的大小，那么空间大小比上下限值，算出来的区间是[1k-2k]
             * 我们没办法保证这个区间的第一个元素可以满足1.5k的需求，因为可能是1.1k，1.2k
             */
            if (index < INTERVALS) {
                index++;
            }
            while (index <= INTERVALS) {
                //如果当前的区间号没有页就找更大的页返回
                if (pageInfoList[index].size() == 0) {
                    index++;
                    continue;
                }
                return pageInfoList[index].remove(0);
            }
//            //如果没找到直接抛异常
//            throw new YoungSQLException(ErrorMsg.DATABASE_BUSY_EXCEPTION);
        } finally {
            lock.unlock();
        }
        return null;
    }
}
